package qy.qyalgotrader.utils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections4.OrderedMap;
import org.apache.commons.collections4.map.ListOrderedMap;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import qy.jalgotrade.utils.CommonUtils;
import qy.qyalgotrader.utils.apiservice.FieldDataType;
import qy.qyalgotrader.utils.apiservice.ts.TsData;
import qy.qyalgotrader.utils.apiservice.wind.WindData;
import tech.tablesaw.api.ColumnType;
import tech.tablesaw.api.Row;
import tech.tablesaw.api.Table;
import tech.tablesaw.columns.dates.DateColumnType;
import tech.tablesaw.columns.datetimes.DateTimeColumnType;
import tech.tablesaw.columns.numbers.DoubleColumnType;
import tech.tablesaw.columns.numbers.IntColumnType;
import tech.tablesaw.columns.numbers.LongColumnType;
import tech.tablesaw.columns.times.TimeColumnType;

/**
 * Helper for qy::qyalgotrader::utils::apiservice
 * 
 * @author qy
 *
 */
public final class Helper {

	private static final Logger logger = LoggerFactory.getLogger(Helper.class);

	private final static Pattern DATE_DOUBLE_PATTERN = Pattern.compile("(\\d{8})(\\.\\d*)?");

	private final static Pattern TIME_DOUBLE_PATTERN = Pattern.compile("(\\d{1,6})(\\.\\d*)?");

	/**
	 * ("YYYYMMDD.0", "HHMMSS.0") -&gt; LocalDateTime
	 * 
	 * @param dtDoubleStr dtDoubleStr
	 * @param tmDoubleStr tmDoubleStr
	 * @return LocalDateTime
	 */
	public static LocalDateTime makeDateTime(String dtDoubleStr, String tmDoubleStr) {

		Matcher mDt = DATE_DOUBLE_PATTERN.matcher(dtDoubleStr);
		Matcher mTm = TIME_DOUBLE_PATTERN.matcher(tmDoubleStr);
		if (!mDt.matches() || !mTm.matches()) {
			throw new IllegalArgumentException(
					String.format("Unrecognized date or time string: %s, %s", dtDoubleStr, tmDoubleStr));
		}

		String dtStr = mDt.group(1);
		String tmStr = mTm.group(1);
		if (tmStr.length() < 6) {
			tmStr = "000000".substring(0, 6 - tmStr.length()) + tmStr;
		}
		return LocalDateTime.parse(dtStr + " " + tmStr, Constants.DATE_TIME_FORMAT_YMD_HMS);
	}

	/**
	 * 
	 * @param fieldDataType FieldDataType
	 * @return ColumnType
	 */
	public static ColumnType fieldDataTypeToColumnType(String fieldDataType) {

		if (FieldDataType.String.name().equals(fieldDataType)) {
			return ColumnType.STRING;
		} else if (FieldDataType.Double.name().equals(fieldDataType)) {
			return ColumnType.DOUBLE;
		} else if (FieldDataType.Int.name().equals(fieldDataType)) {
			return ColumnType.INTEGER;
		} else if (FieldDataType.Long.name().equals(fieldDataType)) {
			return ColumnType.LONG;
		} else if (FieldDataType.Date.name().equals(fieldDataType)) {
			return ColumnType.LOCAL_DATE;
		} else if (FieldDataType.Time.name().equals(fieldDataType)) {
			return ColumnType.LOCAL_TIME;
		} else if (FieldDataType.DateTime.name().equals(fieldDataType)) {
			return ColumnType.LOCAL_DATE_TIME;
		} else {
			throw new IllegalArgumentException("Unsupported fieldDataType: " + fieldDataType);
		}
	}

	/**
	 * 
	 * @param wData wData
	 * @return Table
	 */
	public static Table windDataToDataFrameNoType(WindData wData) {

		Table df = null;
		List<String> cols = new ArrayList<>();
		boolean colDate = false;
		if (wData.errorCode == 0) {
			if (wData.fields.length > 0 && wData.fields.length == wData.fieldDataTypes.length
					&& wData.times.length > 0) {
				try {
					LocalDateTime.parse(wData.times[0], Constants.DATE_TIME_FORMAT_US_OPT);
				} catch (DateTimeParseException e) {
					colDate = true;
				}
				// times 为 Date, 为 DataFrame 添加首列 Constants.DATE:
				if (colDate) {
					if (ArrayUtils.indexOf(wData.fields, Constants.DATE) < 0) {
						cols.add(Constants.DATE);
					}
				} else {
					// times 为 DateTime, 为 DataFrame 添加首列 Constants.DATE_TIME:
					if (ArrayUtils.indexOf(wData.fields, Constants.DATE_TIME) < 0) {
						cols.add(Constants.DATE_TIME);
					}
				}
				cols.addAll(Arrays.asList(wData.fields));
				df = CommonUtils.makeTable(cols);
				for (int j = 0; j < wData.times.length; j++) { // j: times -> dim_0
					Row aRow = df.appendRow();
					if (colDate) {
						if (ArrayUtils.indexOf(wData.fields, Constants.DATE) < 0) {
							aRow.setString(Constants.DATE, wData.times[j]);
						}
					} else {
						if (ArrayUtils.indexOf(wData.fields, Constants.DATE_TIME) < 0) {
							aRow.setString(Constants.DATE_TIME, wData.times[j]);
						}
					}
					for (int i = 0; i < wData.fields.length; i++) { // i: fields -> dim_1
						aRow.setString(wData.fields[i], wData.data[i][j]);
					}
				}
			} else {
				logger.info(
						"WindData with NO proper fields, fieldDataTypes or times, can NOT be convert to DataFrame.");
			}
		} else {
			logger.info("WindData with errorCode != 0, can NOT be convert to DataFrame.");
		}
		return df;
	}

	/**
	 * 
	 * @param wData wData
	 * @return Table
	 */
	public static Table windDataToDataFrame(WindData wData) {

		Table df = null;
		OrderedMap<String, ColumnType> cols = new ListOrderedMap<>();
		boolean colDate = false;
		if (wData.errorCode == 0) {
			if (wData.fields.length > 0 && wData.fields.length == wData.fieldDataTypes.length
					&& wData.times.length > 0) {
				try {
					LocalDateTime.parse(wData.times[0], Constants.DATE_TIME_FORMAT_US_OPT);
				} catch (DateTimeParseException e) {
					colDate = true;
				}
				// times 为 Date, 为 DataFrame 添加首列 Constants.DATE:
				if (colDate) {
					if (ArrayUtils.indexOf(wData.fields, Constants.DATE) < 0) {
						cols.put(Constants.DATE, ColumnType.LOCAL_DATE);
					}
				} else {
					// times 为 DateTime, 为 DataFrame 添加首列 Constants.DATE_TIME:
					if (ArrayUtils.indexOf(wData.fields, Constants.DATE_TIME) < 0) {
						cols.put(Constants.DATE_TIME, ColumnType.LOCAL_DATE_TIME);
					}
				}
				for (int i = 0; i < wData.fields.length; i++) {
					cols.put(wData.fields[i], fieldDataTypeToColumnType(wData.fieldDataTypes[i]));
				}
				df = CommonUtils.makeTable(cols);
				for (int j = 0; j < wData.times.length; j++) { // j: times -> dim_0
					Row aRow = df.appendRow();
					if (colDate) {
						if (ArrayUtils.indexOf(wData.fields, Constants.DATE) < 0) {
							aRow.setDate(Constants.DATE, LocalDate.parse(wData.times[j], Constants.DATE_FORMAT_STD));
						}
					} else {
						if (ArrayUtils.indexOf(wData.fields, Constants.DATE_TIME) < 0) {
							aRow.setDateTime(Constants.DATE_TIME, LocalDateTime.parse(wData.times[j], Constants.DATE_TIME_FORMAT_US_OPT));
						}
					}
					for (int i = 0; i < wData.fields.length; i++) { // i: fields -> dim_1
						ColumnType colType = cols.get(wData.fields[i]);
						if (colType instanceof DoubleColumnType) {
							aRow.setDouble(wData.fields[i], Double.parseDouble(wData.data[i][j]));
						} else if (colType instanceof IntColumnType) {
							aRow.setInt(wData.fields[i], Integer.parseInt(wData.data[i][j]));
						} else if (colType instanceof LongColumnType) {
							aRow.setLong(wData.fields[i], Long.parseLong(wData.data[i][j]));
						} else if (colType instanceof DateColumnType) {
							aRow.setDate(wData.fields[i], LocalDate.parse(wData.data[i][j], Constants.DATE_FORMAT_STD));
						} else if (colType instanceof TimeColumnType) {
							aRow.setTime(wData.fields[i], LocalTime.parse(wData.data[i][j], Constants.TIME_FORMAT_US_OPT));
						} else if (colType instanceof DateTimeColumnType) {
							aRow.setDateTime(wData.fields[i], LocalDateTime.parse(wData.data[i][j], Constants.DATE_TIME_FORMAT_US_OPT));
						} else { // String
							aRow.setString(wData.fields[i], wData.data[i][j]);
						}
					}
				}
			} else {
				logger.info(
						"WindData with NO proper fields, fieldDataTypes or times, can NOT be convert to DataFrame.");
			}
		} else {
			logger.info("WindData with errorCode != 0, can NOT be convert to DataFrame.");
		}
		return df;
	}

	/**
	 * 
	 * @param tData tData
	 * @return Table
	 */
	public static Table tsDataToDataFrameNoType(TsData tData) {

		Table df = null;
		List<String> cols;
		switch ((int) tData.errorCode) {
			case 0:
				cols = Arrays.asList(tData.colNames);
				df = CommonUtils.makeTable(cols);
				for (int i = 0; i < tData.data.length; i++) {
					Row aRow = df.appendRow();
					for (int j = 0; j < tData.colNames.length; j++) {
						aRow.setString(tData.colNames[j], tData.data[i][j]);
					}
				}
				break;

			case -1: // "no data": empty DataFrame
				cols = Arrays.asList(tData.colNames);
				df = CommonUtils.makeTable(cols);
				break;

			default: // 其他错误代码, 返回 null;
				break;
		}
		return df;
	}

	/**
	 * 
	 * @param tData tData
	 * @return Table
	 */
	public static Table tsDataToDataFrame(TsData tData) {

		Table df = null;
		OrderedMap<String, ColumnType> cols = new ListOrderedMap<>();
		switch ((int) tData.errorCode) {
			case 0:
				for (int i = 0; i < tData.fieldDataTypes.length; i++) {
					cols.put(tData.colNames[i], fieldDataTypeToColumnType(tData.fieldDataTypes[i].dataType));
				}
				df = CommonUtils.makeTable(cols);
				for (int i = 0; i < tData.data.length; i++) {
					Row aRow = df.appendRow();
					for (int j = 0; j < tData.colNames.length; j++) {
						ColumnType colType = cols.get(tData.colNames[j]);
						if (colType instanceof DoubleColumnType) {
							aRow.setDouble(j, Double.parseDouble(tData.data[i][j]));
						} else if (colType instanceof IntColumnType) {
							aRow.setInt(j, Integer.parseInt(tData.data[i][j]));
						} else if (colType instanceof LongColumnType) {
							aRow.setLong(j, Long.parseLong(tData.data[i][j]));
						} else if (colType instanceof DateColumnType) {
							aRow.setDate(j, LocalDate.parse(tData.data[i][j],
									Constants.genCtimeFormat(tData.fieldDataTypes[j].fmt)));
						} else if (colType instanceof TimeColumnType) {
							aRow.setTime(j, LocalTime.parse(tData.data[i][j],
									Constants.genCtimeFormat(tData.fieldDataTypes[j].fmt)));
						} else if (colType instanceof DateTimeColumnType) {
							aRow.setDateTime(j, LocalDateTime.parse(tData.data[i][j],
									Constants.genCtimeFormat(tData.fieldDataTypes[j].fmt)));
						} else { // String
							aRow.setString(j, tData.data[i][j]);
						}
					}
				}
				break;

			case -1: // "no data": empty DataFrame
				for (int i = 0; i < tData.fieldDataTypes.length; i++) {
					cols.put(tData.colNames[i], fieldDataTypeToColumnType(tData.fieldDataTypes[i].dataType));
				}
				df = CommonUtils.makeTable(cols);
				break;

			default:
				break;
		}
		return df;
	}

	/**
	 * 
	 * @param wd wd
	 * @return toString
	 */
	public static String toString(WindData wd) {

		// @formatter:off
		String tp = "WindData: {\n" +
				"    errorCode: %s\n" +
				"    requestId: %s\n" +
				"    codes: %s\n" +
				"    fields: %s\n" +
				"    times: %s\n" +
				"    fieldDataTypes: %s\n" +
				"    data (%s rec.): %s\n}";

		return String.format(tp, wd.errorCode, wd.requestId,
				ArrayUtils.toString(wd.codes),
				ArrayUtils.toString(wd.fields),
				ArrayUtils.toString(wd.times),
				ArrayUtils.toString(wd.fieldDataTypes),
				wd.data.length, ArrayUtils.toString(wd.data));
		// @formatter:on
	}

	/**
	 *
	 * @param tData tData
	 * @return toString
	 */
	public static String toString(TsData tData) {

		// @formatter:off
		String tp = "TsData: {\n" +
				"    errorCode: %s\n" +
				"    errorMsg: %s\n" +
				"    codes: %s\n" +
				"    colNames: %s\n" +
				"    indexes: %s\n" +
				"    fieldDataTypes: %s\n" +
				"    data (%s rec.): %s\n}";

		StringBuilder fdtStr = new StringBuilder();
		Arrays.asList(tData.fieldDataTypes).forEach(fti -> fdtStr.append("(dataType:").append(fti.dataType)
				.append(",fmt:").append(fti.fmt).append("),"));
		return String.format(tp, tData.errorCode, tData.errorMsg,
				ArrayUtils.toString(tData.codes),
				ArrayUtils.toString(tData.colNames),
				ArrayUtils.toString(tData.indexes),
				"{" + fdtStr.substring(0, fdtStr.length() - 1) + "}",
				tData.data.length, ArrayUtils.toString(tData.data));
		// @formatter:on
	}

	/**
	 * 
	 */
	private Helper() {

	}
}
